今天不再新增一個核心名詞,而是問現有工具能不能區分四種不同工作:持續計算、等待事件、擴充記憶體,以及反覆進行系統呼叫。
如果資料欄位沒有清楚的定義,最後只會得到一張有很多數字、卻無法解釋的表。
我們會讓同一個行程在工作前後各呼叫一次 pstat,再用差值整理結果。
這個 Monitor 是命令列觀察與驗證工具,不是全系統即時儀表板。
在 xv6 checkout 新增 user/observer.c,並將 $U/_observer\ 加入 Makefile 的 UPROGS:
#include "kernel/types.h"
#include "kernel/pstat.h"
#include "user/user.h"
int
main(int argc, char **argv)
{
if (argc != 2) {
fprintf(2, "usage: observer cpu|wait|memory|syscall\n");
exit(1);
}
struct pstat before, after;
if (pstat(&before, sizeof(before)) < 0)
exit(1);
char *allocated = 0;
int expected_syscalls = -1;
if (strcmp(argv[1], "cpu") == 0) {
volatile uint64 value = 1;
for (uint64 i = 0; i < 30000000; i++)
value = value * 1664525 + 1013904223;
expected_syscalls = 1;
} else if (strcmp(argv[1], "wait") == 0) {
if (pause(20) < 0)
exit(1);
expected_syscalls = 2;
} else if (strcmp(argv[1], "memory") == 0) {
allocated = sbrk(4 * 4096);
if (allocated == SBRK_ERROR)
exit(1);
for (int i = 0; i < 4 * 4096; i += 4096)
allocated[i] = 'M';
expected_syscalls = 2;
} else if (strcmp(argv[1], "syscall") == 0) {
for (int i = 0; i < 1000; i++)
getpid();
expected_syscalls = 1001;
} else {
fprintf(2, "unknown workload\n");
exit(1);
}
if (pstat(&after, sizeof(after)) < 0)
exit(1);
uint64 calls = after.syscalls - before.syscalls;
printf("work=%s pid=%d scheduled=%lu calls=%lu sleep=%lu wake=%lu yield=%lu size_before=%lu size_after=%lu\n",
argv[1], after.pid, after.scheduled - before.scheduled,
calls, after.sleeps - before.sleeps,
after.wakes - before.wakes, after.yields - before.yields,
before.size_bytes, after.size_bytes);
int failed = before.pid != after.pid || calls != expected_syscalls;
if (allocated && after.size_bytes != before.size_bytes + 4 * 4096)
failed = 1;
if (allocated && sbrk(-4 * 4096) == SBRK_ERROR)
failed = 1;
printf("observer checks=%s\n", failed ? "failed" : "ok");
exit(failed);
}
列印放在第二次取樣之後,因此 write 不會混入被比較的區間。
第二次 pstat 本身已先被 dispatcher 計入,這就是每種工作都額外多 1 次 syscall 的原因。
記憶體釋放也放在取樣之後,確保我們先看得到大小增加,再恢復配置。
在 30-days-os-kernel/examples/xv6-riscv/ 執行:
git rev-parse HEAD
git diff --stat
make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu
於 xv6 Shell 依序執行:
pstat
observer cpu
observer wait
observer memory
observer syscall
pipewait
racetest 2
trace pidprobe
不要先用 trace observer syscall 量測,因為它會為每次 getpid 印出一行,明顯改變工作條件。
最後的 trace pidprobe 只檢查選擇性追蹤是否仍可使用。
| 工作 | 可檢查的關係 | 不固定的數值 |
|---|---|---|
| cpu | 取樣區間 syscall 差值為 1 | dispatch 與 timer yield 次數 |
| wait | syscall 差值為 2 | 實際睡眠與喚醒次數 |
| memory | syscall 差值為 2,sz 增加 16,384 bytes | 實際 PA 與排程次數 |
| syscall | syscall 差值為 1,001 | 完成時間與是否被搶占 |
wait 的結果通常包含 sleep/wake,但不要把「等待 20 ticks」直接等同於「剛好睡眠 20 次」。
事件可能在進入睡眠前已經到達,計時與排程也會改變狀態轉移次數。
memory 的差值驗證的是 sz,不是 RSS。
本程式使用固定版本的 eager sbrk() 並碰觸每一頁,沒有宣稱已完成 lazy allocation、page replacement 或記憶體壓力測試。
結束 QEMU,再以 CPUS=3 啟動,重跑同一組命令。
需要多個可執行工作時,可執行 Day 23 的 cpuburn,或在 Shell 分別執行 observer cpu & 與另一份 observer cpu。
每筆結果以自己的 PID 和前後差值為單位,不把兩個 PID 的累計值相減。
race 測試必須仍然逐次執行,不能同時啟動兩份含 reset 的 racetest。
這是教學 API 的限制,不應被批次工具忽略。
在文章 repository 另存一份測試紀錄,至少包含以下資訊:
不要只保留裁切後的成功截圖,失敗紀錄同樣能幫助後續定位問題。
| 責任 | 自己寫過的最小版本 | 現在能觀察的 xv6 行為 |
|---|---|---|
| 執行切換 | 保存 ra/sp/s 暫存器 | CPU 與行程 context 交接 |
| 排程 | 合作式循環選擇 | timer yield 與多 hart dispatch |
| 位址空間 | Sv32 映射與 walker | Sv39、exec 與特殊頁面 |
| 系統呼叫 | ecall 與簡單 dispatcher | 使用者 stub、trampoline 與 pstat |
| 等待 | 手動等待與喚醒 | pipe/timer 的等待協定 |
| 觀察 | Console 與單次 assertion | 結構化統計、可選 trace 與差值測試 |
我們仍沒有實作全系統原子快照、CPU 使用率估算或持久化 trace,也沒有把單次 QEMU 成功等同於核心已無缺陷。
但現在每個觀察欄位都有更新位置,每個測試都有可檢查的關係,已經能用程式解釋核心的一段實際行為。
本日新增 user/observer.c、Makefile 項目與自己的測試報告。
建議 commit:
day30: validate kernel observer across four workloads
30 天的終點不是記住更多名詞,而是遇到一個核心行為時,知道要看哪份狀態、在哪個邊界量測,以及證據還不足以支持哪些結論。